Классификация текстов: спам-фильтр для SMS

В этом задании вам предстоит взять открытый датасет с SMS-сообщениями, размеченными на спам ("spam") и не спам ("ham"), построить на нем классификатор текстов на эти два класса, оценить его качество с помощью кросс-валидации, протестировать его работу на отдельных примерах, и посмотреть, что будет происходить с качеством, если менять параметры вашей модели.


In [1]:
from __future__ import division, print_function

import numpy as np

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline

from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
1. Загрузите датасет. Описание датасета можно посмотреть здесь:

http://www.dt.fee.unicamp.br/~tiago/smsspamcollection


In [2]:
# reading dataset
with open('smsspamcollection/SMSSpamCollection.txt', 'r') as f:
    sms = f.read().splitlines()

In [3]:
sms[0]
sms[11]


Out[3]:
'ham\tGo until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...'
Out[3]:
'spam\tSIX chances to win CASH! From 100 to 20,000 pounds txt> CSH11 and send to 87575. Cost 150p/day, 6days, 16+ TsandCs apply Reply HL 4 info'
2. Подготовьте для дальнейшей работы два списка: список текстов в порядке их следования в датасете и список соответствующих им меток классов. В качестве метки класса используйте 1 для спама и 0 для "не спама".

In [4]:
# sms_label - labels('ham', 'spam')
# sms_text - sms texts
sms_label, sms_text = zip(*[line.split('\t') for line in sms])

# sms_label_bi - 0 for ham, 1 for spam
sms_label_bi = np.array([0 if t=='ham' else 1 for t in sms_label])

In [5]:
sms_label[0:5]
sms_label_bi[0:5]
sms_text[0:5]


Out[5]:
('ham', 'ham', 'spam', 'ham', 'ham')
Out[5]:
array([0, 0, 1, 0, 0])
Out[5]:
('Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...',
 'Ok lar... Joking wif u oni...',
 "Free entry in 2 a wkly comp to win FA Cup final tkts 21st May 2005. Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's",
 'U dun say so early hor... U c already then say...',
 "Nah I don't think he goes to usf, he lives around here though")
3. Используя sklearn.feature_extraction.text.CountVectorizer со стандартными настройками, получите из списка текстов матрицу признаков X.

In [6]:
# CountVectorizer for sms_text
count_vect = CountVectorizer()
X_counts = count_vect.fit_transform(sms_text)
X_counts.shape


Out[6]:
(5574, 8713)
4. Оцените качество классификации текстов с помощью LogisticRegression() с параметрами по умолчанию, используя sklearn.cross_validation.cross_val_score и посчитав среднее арифметическое качества на отдельных fold'ах. Установите random_state=2. Параметр cv задайте равным 10. В качестве метрики качества используйте f1-меру. Получившееся качество - один из ответов, которые потребуются при сдаче задания. Ответ округлить до 1 знака после запятой.

In [7]:
pipeline = Pipeline([('count_vect', CountVectorizer()), ('log_reg', LogisticRegression())])

In [8]:
# cross_val_score for f1 metric for log_reg
score = cross_val_score(pipeline, sms_text, sms_label_bi, scoring='f1', cv=10).mean()
print('Cross_val_score f1: %.4f' % score)


Cross_val_score f1: 0.9333

In [9]:
with open('ans1.txt', 'w') as f:
    ans1 = str(round(score, 1))
    f.write(ans1)
5. А теперь обучите классификатор на всей выборке и спрогнозируйте с его помощью класс для следующих сообщений:

"FreeMsg: Txt: CALL to No: 86888 & claim your reward of 3 hours talk time to use from your phone now! Subscribe6GB"

"FreeMsg: Txt: claim your reward of 3 hours talk time"

"Have you visited the last lecture on physics?"

"Have you visited the last lecture on physics? Just buy this book and you will have all materials! Only 99$"

"Only 99$"


In [10]:
sms_test = ["FreeMsg: Txt: CALL to No: 86888 & claim your reward of 3 hours talk time to use from your phone now! Subscribe6GB",
            "FreeMsg: Txt: claim your reward of 3 hours talk time",
            "Have you visited the last lecture on physics?",
            "Have you visited the last lecture on physics? Just buy this book and you will have all materials! Only 99$",
            "Only 99$"]

In [11]:
# fitting full dataset
pipeline.fit(sms_text, sms_label_bi)


Out[11]:
Pipeline(steps=[('count_vect', CountVectorizer(analyzer=u'word', binary=False, decode_error=u'strict',
        dtype=<type 'numpy.int64'>, encoding=u'utf-8', input=u'content',
        lowercase=True, max_df=1.0, max_features=None, min_df=1,
        ngram_range=(1, 1), preprocessor=None, stop_words=None,
    ...ty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False))])

In [12]:
# predicting labels for sms_test
sms_test_pr = pipeline.predict(sms_test)
sms_test_pr


Out[12]:
array([1, 1, 0, 0, 0])

In [13]:
with open('ans2.txt', 'w') as f:
    ans2 = ' '.join(map(str, sms_test_pr))
    f.write(ans2)
6. Задайте в CountVectorizer параметр ngram_range=(2,2), затем ngram_range=(3,3), затем ngram_range=(1,3). Во всех трех случаях измерьте получившееся в кросс-валидации значение f1-меры, округлите до второго знака после точки, и выпишете результаты через пробел в том же порядке. В данном эксперименте мы пробовали добавлять в признаки n-граммы для разных диапазонов n - только биграммы, только триграммы, и, наконец, все вместе - униграммы, биграммы и триграммы. Обратите внимание, что статистики по биграммам и триграммам намного меньше, поэтому классификатор только на них работает хуже. В то же время это не ухудшает результат сколько-нибудь существенно, если добавлять их вместе с униграммами, т.к. за счет регуляризации линейный классификатор не склонен сильно переобучаться на этих признаках.

In [14]:
# testing log_reg the model in different ngram ranges
ngram_range = [(2,2), (3,3), (1,3)]

scores = []
for ngram in ngram_range:
    pipeline = Pipeline([('count_vect', CountVectorizer(ngram_range=ngram)), ('log_reg', LogisticRegression())])
    score = cross_val_score(pipeline, sms_text, sms_label_bi, scoring='f1', cv=10).mean()
    scores.append(score)
    print('Ngram_range: ', ngram)
    print('Cross-val-score f1: %.4f\n' % score)


Ngram_range:  (2, 2)
Cross-val-score f1: 0.8224

Ngram_range:  (3, 3)
Cross-val-score f1: 0.7250

Ngram_range:  (1, 3)
Cross-val-score f1: 0.9251


In [15]:
with open('ans3.txt', 'w') as f:
    ans3 = ' '.join(map(str, ['%.2f' % sc for sc in scores]))
    f.write(ans3)
7. Повторите аналогичный п.6 эксперимент, используя вместо логистической регрессии MultinomialNB(). Обратите внимание, насколько сильнее (по сравнению с линейным классификатором) наивный Байес страдает от нехватки статистики по биграммам и триграммам.

In [16]:
# testing mult_nb the model in different ngram ranges
scores = []
for ngram in ngram_range:
    pipeline = Pipeline([('count_vect', CountVectorizer(ngram_range=ngram)), ('mn_nb', MultinomialNB())])
    score = cross_val_score(pipeline, sms_text, sms_label_bi, scoring='f1', cv=10).mean()
    scores.append(score)
    print('Ngram_range: ', ngram)
    print('Cross-val-score f1: %.4f\n' % score)


Ngram_range:  (2, 2)
Cross-val-score f1: 0.9337

Ngram_range:  (3, 3)
Cross-val-score f1: 0.8713

Ngram_range:  (1, 3)
Cross-val-score f1: 0.9473


In [17]:
with open('ans4.txt', 'w') as f:
    ans4 = ' '.join(map(str, ['%.2f' % sc for sc in scores]))
    f.write(ans4)

In [18]:
# testing mult_nb the model in different ngram ranges
# !!!another results from above due to pipeline (reason unknown)!!!
scores = []
for ngram in ngram_range:
    X_counts = CountVectorizer(ngram_range=ngram).fit_transform(sms_text)
    mn_nb = MultinomialNB()
    score = cross_val_score(mn_nb, X_counts, sms_label_bi, scoring='f1', cv=10).mean()
    scores.append(score)
    print('Ngram_range: ', ngram)
    print('Cross-val-score f1: %.4f\n' % score)


Ngram_range:  (2, 2)
Cross-val-score f1: 0.6455

Ngram_range:  (3, 3)
Cross-val-score f1: 0.3786

Ngram_range:  (1, 3)
Cross-val-score f1: 0.8879


In [19]:
with open('ans4.txt', 'w') as f:
    ans4 = ' '.join(map(str, ['%.2f' % sc for sc in scores]))
    f.write(ans4)
8. Попробуйте использовать в логистической регрессии в качестве признаков Tf*idf из TfidfVectorizer на униграммах. Повысилось или понизилось качество на кросс-валидации по сравнению с CountVectorizer на униграммах? (напишите в файле с ответом 1, если повысилось, -1, если понизилось, и 0, если изменилось не более чем на 0.01). Обратите внимание, что результат перехода к tf*idf не всегда будет таким - если вы наблюдаете какое-то явление на одном датасете, не надо сразу же его обобщать на любые данные.

In [20]:
# cross_val_score for f1 metric for log_reg with tf_idf
pipeline = Pipeline([('tf_idf', TfidfVectorizer()),
                     ('log_reg', LogisticRegression())])
score = cross_val_score(pipeline, sms_text, sms_label_bi, scoring='f1', cv=10).mean()
print('Cross-val-score f1: %.4f\n' % score)


Cross-val-score f1: 0.8785


In [21]:
with open('ans5.txt', 'w') as f:
    ans5 = str(-1)
    f.write(ans5)